'use strict';

var protocol = require('./constants.js');
var empty = new Buffer(0);

function generate(packet) {
    switch (packet.cmd) {
        case 'connect':
            return connect(packet);
        case 'connack':
            return connack(packet);
        case 'publish':
            return publish(packet);
        case 'puback':
        case 'pubrec':
        case 'pubrel':
        case 'pubcomp':
        case 'unsuback':
            return confirmation(packet);
        case 'subscribe':
            return subscribe(packet);
        case 'suback':
            return suback(packet);
        case 'unsubscribe':
            return unsubscribe(packet);
        case 'pingreq':
        case 'pingresp':
        case 'disconnect':
            return emptyPacket(packet);
        default:
            throw new Error('unknown command');
    }
}

function connect(opts) {
    opts = opts || {};
    var protocolId = opts.protocolId || 'MQTT';
    var protocolVersion = opts.protocolVersion || 4;
    var will = opts.will;
    var clean = opts.clean;
    var keepalive = opts.keepalive || 0;
    var clientId = opts.clientId || '';
    var username = opts.username;
    var password = opts.password;

    if (clean === undefined) {
        clean = true;
    }

    var length = 0;

    // Must be a string and non-falsy
    if (!protocolId ||
         (typeof protocolId !== 'string' && !Buffer.isBuffer(protocolId))) {
        throw new Error('Invalid protocol id');
    } else {
        length += protocolId.length + 2;
    }

    // Must be a 1 byte number
    if (!protocolVersion ||
            'number' !== typeof protocolVersion ||
            protocolVersion > 255 ||
            protocolVersion < 0) {

        throw new Error('Invalid protocol version');
    } else {
        length += 1;
    }

    // ClientId might be omitted in 3.1.1, but only if cleanSession is set to 1
    if ((typeof clientId === 'string' || Buffer.isBuffer(clientId)) &&
         (clientId || protocolVersion == 4) &&
         (clientId || clean)) {

        length += clientId.length + 2;
    } else {

        if (protocolVersion < 4) {

            throw new Error('clientId must be supplied before 3.1.1');
        }

        if (clean == 0) {

            throw new Error('clientId must be given if cleanSession set to 0');
        }
    }

    // Must be a two byte number
    if ('number' !== typeof keepalive ||
            keepalive < 0 ||
            keepalive > 65535) {
        throw new Error('Invalid keepalive');
    } else {
        length += 2;
    }

    // Connect flags
    length += 1;

    // If will exists...
    if (will) {
        // It must be an object
        if ('object' !== typeof will) {
            throw new Error('Invalid will');
        }
        // It must have topic typeof string
        if (!will.topic || 'string' !== typeof will.topic) {
            throw new Error('Invalid will topic');
        } else {
            length += Buffer.byteLength(will.topic) + 2;
        }

        // Payload
        if (will.payload && will.payload) {
            if (will.payload.length >= 0) {
                if ('string' === typeof will.payload) {
                    length += Buffer.byteLength(will.payload) + 2;
                } else {
                    length += will.payload.length + 2;
                }
            } else {
                throw new Error('Invalid will payload');
            }
        } else {
            length += 2;
        }
    }

    // Username
    if (username) {
        if (username.length) {
            length += Buffer.byteLength(username) + 2;
        } else {
            throw new Error('Invalid username');
        }
    }

    // Password
    if (password) {
        if (password.length) {
            length += byteLength(password) + 2;
        } else {
            throw new Error('Invalid password');
        }
    }

    var buffer = new Buffer(1 + calcLengthLength(length) + length);
    var pos = 0;

    // Generate header
    buffer.writeUInt8(protocol.codes['connect'] << protocol.CMD_SHIFT, pos++, true);

    // Generate length
    pos += writeLength(buffer, pos, length);

    // Generate protocol ID
    pos += writeStringOrBuffer(buffer, pos, protocolId);
    buffer.writeUInt8(protocolVersion, pos++, true);

    // Connect flags
    var flags = 0;
    flags |= username ? protocol.USERNAME_MASK : 0;
    flags |= password ? protocol.PASSWORD_MASK : 0;
    flags |= (will && will.retain) ? protocol.WILL_RETAIN_MASK : 0;
    flags |= (will && will.qos) ?
        will.qos << protocol.WILL_QOS_SHIFT : 0;
    flags |= will ? protocol.WILL_FLAG_MASK : 0;
    flags |= clean ? protocol.CLEAN_SESSION_MASK : 0;

    buffer.writeUInt8(flags, pos++, true);

    // Keepalive
    pos += writeNumber(buffer, pos, keepalive);

    // Client ID
    pos += writeStringOrBuffer(buffer, pos, clientId);

    // Will
    if (will) {
        pos += writeString(buffer, pos, will.topic);
        pos += writeStringOrBuffer(buffer, pos, will.payload);
    }

    // Username and password
    if (username) {
        pos += writeStringOrBuffer(buffer, pos, username);
    }

    if (password) {
        pos += writeStringOrBuffer(buffer, pos, password);
    }

    return buffer;
}

function connack(opts) {
    opts = opts || {};
    var rc = opts.returnCode;

    // Check return code
    if ('number' !== typeof rc) {
        throw new Error('Invalid return code');
    }

    var buffer = new Buffer(4);
    var pos = 0;

    buffer.writeUInt8(protocol.codes['connack'] << protocol.CMD_SHIFT, pos++, true);
    pos += writeLength(buffer, pos, 2);
    buffer.writeUInt8(opts.sessionPresent && protocol.SESSIONPRESENT_MASK || 0, pos++, true);
    buffer.writeUInt8(rc, pos++, true);

    return buffer;
}

function publish(opts) {
    opts = opts || {};
    var dup = opts.dup ? protocol.DUP_MASK : 0;
    var qos = opts.qos;
    var retain = opts.retain ? protocol.RETAIN_MASK : 0;
    var topic = opts.topic;
    var payload = opts.payload || empty;
    var id = opts.messageId;

    var length = 0;

    // Topic must be a non-empty string or Buffer
    if (typeof topic === 'string') {
        length += Buffer.byteLength(topic) + 2;
    } else if (Buffer.isBuffer(topic)) {
        length += topic.length + 2;
    } else {
        throw new Error('Invalid topic');
    }

    // get the payload length
    if (!Buffer.isBuffer(payload)) {
        length += Buffer.byteLength(payload);
    } else {
        length += payload.length;
    }

    // Message id must a number if qos > 0
    if (qos && 'number' !== typeof id) {
        throw new Error('Invalid message id');
    } else if (qos) {
        length += 2;
    }

    var buffer = new Buffer(1 + calcLengthLength(length) + length);
    var pos = 0;

    // Header
    buffer.writeUInt8(
        protocol.codes['publish'] << protocol.CMD_SHIFT |
        dup |
        qos << protocol.QOS_SHIFT |
        retain, pos++, true);

    // Remaining length
    pos += writeLength(buffer, pos, length);

    // Topic
    pos += writeStringOrBuffer(buffer, pos, topic);

    // Message ID
    if (qos > 0) {
        pos += writeNumber(buffer, pos, id);
    }

    // Payload
    if (!Buffer.isBuffer(payload)) {
        writeStringNoPos(buffer, pos, payload);
    } else {
        writeBuffer(buffer, pos, payload);
    }

    return buffer;
}

/* Puback, pubrec, pubrel and pubcomp */
function confirmation(opts) {
    opts = opts || {};
    var type = opts.cmd || 'puback';
    var id = opts.messageId;
    var dup = (opts.dup && type === 'pubrel') ? protocol.DUP_MASK : 0;
    var qos = 0;

    if (type === 'pubrel') {
        qos = 1;
    }

    // Check message ID
    if ('number' !== typeof id) {
        throw new Error('Invalid message id');
    }

    var buffer = new Buffer(4);
    var pos = 0;

    // Header
    buffer[pos++] =
        protocol.codes[type] << protocol.CMD_SHIFT |
        dup |
        qos << protocol.QOS_SHIFT;

    // Length
    pos += writeLength(buffer, pos, 2);

    // Message ID
    pos += writeNumber(buffer, pos, id);

    return buffer;
}

function subscribe(opts) {
    opts = opts || {};
    var dup = opts.dup ? protocol.DUP_MASK : 0;
    var id = opts.messageId;
    var subs = opts.subscriptions;

    var length = 0;

    // Check mid
    if ('number' !== typeof id) {
        throw new Error('Invalid message id');
    } else {
        length += 2;
    }
    // Check subscriptions
    if ('object' === typeof subs && subs.length) {
        for (var i = 0; i < subs.length; i += 1) {
            var topic = subs[i].topic;
            var qos = subs[i].qos;

            if ('string' !== typeof topic) {
                throw new Error('Invalid subscriptions - invalid topic');
            }
            if ('number' !== typeof qos) {
                throw new Error('Invalid subscriptions - invalid qos');
            }

            length += Buffer.byteLength(topic) + 2 + 1;
        }
    } else {
        throw new Error('Invalid subscriptions');
    }

    var buffer = new Buffer(1 + calcLengthLength(length) + length);
    var pos = 0;

    // Generate header
    buffer.writeUInt8(
        protocol.codes['subscribe'] << protocol.CMD_SHIFT |
        dup |
        1 << protocol.QOS_SHIFT, pos++, true);

    // Generate length
    pos += writeLength(buffer, pos, length);

    // Generate message ID
    pos += writeNumber(buffer, pos, id);

    // Generate subs
    for (var i = 0; i < subs.length; i++) {
        var sub = subs[i];
        var topic = sub.topic;
        var qos = sub.qos;

        // Write topic string
        pos += writeString(buffer, pos, topic);
        // Write qos
        buffer.writeUInt8(qos, pos++, true);
    }

    return buffer;
}

function suback(opts) {
    opts = opts || {};
    var id = opts.messageId;
    var granted = opts.granted;

    var length = 0;

    // Check message id
    if ('number' !== typeof id) {
        throw new Error('Invalid message id');
    } else {
        length += 2;
    }
    // Check granted qos vector
    if ('object' === typeof granted && granted.length) {
        for (var i = 0; i < granted.length; i += 1) {
            if ('number' !== typeof granted[i]) {
                throw new Error('Invalid qos vector');
            }
            length += 1;
        }
    } else {
        throw new Error('Invalid qos vector');
    }

    var buffer = new Buffer(1 + calcLengthLength(length) + length);
    var pos = 0;

    // Header
    buffer.writeUInt8(protocol.codes['suback'] << protocol.CMD_SHIFT, pos++, true);

    // Length
    pos += writeLength(buffer, pos, length);

    // Message ID
    pos += writeNumber(buffer, pos, id);

    // Subscriptions
    for (var i = 0; i < granted.length; i++) {
        buffer.writeUInt8(granted[i], pos++, true);
    }

    return buffer;
}

function unsubscribe(opts) {
    opts = opts || {};
    var id = opts.messageId;
    var dup = opts.dup ? protocol.DUP_MASK : 0;
    var unsubs = opts.unsubscriptions;

    var length = 0;

    // Check message id
    if ('number' !== typeof id) {
        throw new Error('Invalid message id');
    } else {
        length += 2;
    }
    // Check unsubs
    if ('object' === typeof unsubs && unsubs.length) {
        for (var i = 0; i < unsubs.length; i += 1) {
            if ('string' !== typeof unsubs[i]) {
                throw new Error('Invalid unsubscriptions');
            }
            length += Buffer.byteLength(unsubs[i]) + 2;
        }
    } else {
        throw new Error('Invalid unsubscriptions');
    }

    var buffer = new Buffer(1 + calcLengthLength(length) + length);
    var pos = 0;

    // Header
    buffer[pos++] =
        protocol.codes['unsubscribe'] << protocol.CMD_SHIFT |
        dup |
        1 << protocol.QOS_SHIFT;

    // Length
    pos += writeLength(buffer, pos, length);

    // Message ID
    pos += writeNumber(buffer, pos, id);

    // Unsubs
    for (var i = 0; i < unsubs.length; i++) {
        pos += writeString(buffer, pos, unsubs[i]);
    }

    return buffer;
}

function emptyPacket(opts) {
    var buf = new Buffer(2);
    buf[0] = protocol.codes[opts.cmd] << 4;
    buf[1] = 0;
    return buf;
}

/**
 * calcLengthLength - calculate the length of the remaining
 * length field
 *
 * @api private
 */
function calcLengthLength(length) {
    if (length >= 0 && length < 128) {
        return 1;
    } else if (length >= 128 && length < 16384) {
        return 2;
    } else if (length >= 16384 && length < 2097152) {
        return 3;
    } else if (length >= 2097152 && length < 268435456) {
        return 4;
    } else {
        return 0;
    }
}

/**
 * writeLength - write an MQTT style length field to the buffer
 *
 * @param <Buffer> buffer - destination
 * @param <Number> pos - offset
 * @param <Number> length - length (>0)
 * @returns <Number> number of bytes written
 *
 * @api private
 */

function writeLength(buffer, pos, length) {
    var digit = 0;
    var origPos = pos;

    do {
        digit = length % 128 | 0;
        length = length / 128 | 0;
        if (length > 0) {
            digit |= 0x80;
        }
        buffer.writeUInt8(digit, pos++, true);
    } while (length > 0);

    return pos - origPos;
}

/**
 * writeString - write a utf8 string to the buffer
 *
 * @param <Buffer> buffer - destination
 * @param <Number> pos - offset
 * @param <String> string - string to write
 * @return <Number> number of bytes written
 *
 * @api private
 */

function writeString(buffer, pos, string) {
    var strlen = Buffer.byteLength(string);
    writeNumber(buffer, pos, strlen);

    writeStringNoPos(buffer, pos + 2, string);

    return strlen + 2;
}

function writeStringNoPos(buffer, pos, string) {
    buffer.write(string, pos);
}

/**
 * write_buffer - write buffer to buffer
 *
 * @param <Buffer> buffer - dest buffer
 * @param <Number> pos - offset
 * @param <Buffer> src - source buffer
 * @return <Number> number of bytes written
 *
 * @api private
 */

function writeBuffer(buffer, pos, src) {
    src.copy(buffer, pos);
    return src.length;
}

/**
 * writeNumber - write a two byte number to the buffer
 *
 * @param <Buffer> buffer - destination
 * @param <Number> pos - offset
 * @param <String> number - number to write
 * @return <Number> number of bytes written
 *
 * @api private
 */
function writeNumber(buffer, pos, number) {
    buffer.writeUInt8(number >> 8, pos, true);
    buffer.writeUInt8(number & 0x00FF, pos + 1, true);

    return 2;
}

/**
 * writeStringOrBuffer - write a String or Buffer with the its length prefix
 *
 * @param <Buffer> buffer - destination
 * @param <Number> pos - offset
 * @param <String> toWrite - String or Buffer
 * @return <Number> number of bytes written
 */
function writeStringOrBuffer(buffer, pos, toWrite) {
    var written = 0;

    if (toWrite && typeof toWrite === 'string') {
        written += writeString(buffer, pos + written, toWrite);
    } else if (toWrite) {
        written += writeNumber(buffer, pos + written, toWrite.length);
        written += writeBuffer(buffer, pos + written, toWrite);
    } else {
        written += writeNumber(buffer, pos + written, 0);
    }

    return written;
}

function byteLength(bufOrString) {
    if (Buffer.isBuffer(bufOrString)) {
        return bufOrString.length;
    } else {
        return Buffer.byteLength(bufOrString);
    }
}

module.exports = generate;
